﻿using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using Nemerle.Assertions;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Imperative;
using System.Web.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.IO;
using NRails.Macros.Model;

namespace NRails.Macros
{
  module InitConstants
  {
    mutable mvcBuilder : TypeBuilder;
    
    [Record]
    variant ClassType
    {
        public tb : TypeBuilder;
        
        | Controller 
        | Model 
        | DbManager 
    }
    
    public Init(_ : Typer, manager: ManagerClass, phase : Phase) : void
    {
       def nss = manager.CoreEnv.NameTree.NamespaceTree;

       mvcBuilder = match(phase, mvcBuilder) 
       {
           | (_, null)
           | (Phase.BeforeInheritance, _) => 
               manager.CoreEnv.Define(<[decl:
                public module MVC
                {
                }
               ]>)
           | _ => mvcBuilder;
       }
       
        def ns = $"$(manager.Options.RootNamespace).Models".Split('.').ToNList();
        def getClasses()
        {
            def tbToClassType(tb)
            {
                | tb when isController(tb) => Some(ClassType.Controller(tb))
                | tb when isDbManager(tb) => Some(ClassType.DbManager(tb))
                | tb when isModel(ns, tb) => Some(ClassType.Model(tb))
                | _ => None()
            }
            nss.GetTypeBuilders().Map(tbToClassType).Filter(_.IsSome).Map(_.Value);
        }

        def engine = mvcBuilder.Manager.GetNRailsEngine();
        match (phase)
        {
            | Phase.BeforeInheritance =>
                when (!engine.Cfg.NoDb)
                {
                    def dbManagers = getClasses().MapFiltered(t => t is ClassType.DbManager, t => t.tb);
                    when (dbManagers.Length == 0)
                        Message.FatalError("Class with attribute DatabaseManager not found");
                    when (dbManagers.Length > 1)
                        Message.FatalError($"Multiple DatabaseManagers: ..$(dbManagers.Map(_.FullName))");
                    DatabaseManagerImpl.SetDbType(dbManagers.First());
                    DatabaseManagerImpl.GenerateHelpers(dbManagers.First());
                }
            | Phase.WithTypedMembers =>
                def classes = getClasses();
                when (!engine.Cfg.NoDb)
                {
                    ProcessModels(classes.MapFiltered(_ is ClassType.Model, _.tb), mvcBuilder, ns);
                }
                classes.MapFiltered(_ is ClassType.Controller, _.tb)
                    .Iter(ProcessController(_, mvcBuilder));

                ProcessContent(mvcBuilder);
                ProcessScripts(mvcBuilder);
                hideEquals(mvcBuilder);
                mvcBuilder.Compile();
                mvcBuilder = null
            | _ => ()
        }
    }

    isModel(ns : list[string], tb : TypeBuilder) : bool
    {
        tb.NamespaceNode.FullName.FirstN(tb.NamespaceNode.FullName.Length - 1) == ns
    }
    
    controllerPredicate : Regex = Regex("Controller$");
    isController(tb : TypeBuilder) : bool
    {
        controllerPredicate.IsMatch(tb.Name) && !tb.IsAbstract
    }
    
    isDbManager(tb : TypeBuilder) : bool
    {
        tb.GetModifiers().custom_attrs.Any(fun (attr)
        {
            | <[ DatabaseManager ]> => true
            | _ => false
        })
    }

    ProcessModels(mutable tbs : list[TypeBuilder], mvcBuilder : TypeBuilder, ns : list[string]) : void
    {
        def engine = mvcBuilder.Manager.GetNRailsEngine();
        try 
        {
            def dbDriver = engine.CreateDbDriver();
            def schemaDriver = dbDriver.CreateSchemaDriver();
            def schema = schemaDriver.LoadExistingSchema(engine.Cfg.ConnectionString.ConnectionString);
            def tableNames = schema.Tables.Map(_.Name).Remove("SchemaMigrations").Where(t => NRailsInitsImpl.FilterTable(t));
            
            def dbManagerBuilder = DatabaseManagerImpl.GetDbType(mvcBuilder.Manager);
            
            tbs = tbs.Filter(t => tableNames.Contains(t.Name));
            
            tbs.Iter(tb => {
                def getName(typeBuilder, paramName)
                {
                    def matchArgs(args, paramName) : string
                    {
                        def getAllValues(args : list[PExpr])
                        {
                            def literals = args.TakeWhile(t => t is PExpr.Literal);
                            def named = args.Except(literals);

                            (match (literals)
                            {
                                | PExpr.Literal(Literal.String(value)) :: [] when paramName == "Table" => value :: [];
                                | _ => []
                            })
                            .Append(
                            match (named)
                            {
                                | PExpr.Assign(PExpr.Ref(para), PExpr.Literal(Literal.String(value))) :: tail when para.Id == paramName => value :: getAllValues(tail)
                                | _ => []
                            });
                        }

                        match (getAllValues(args))
                        {
                            | value :: [] => value
                            | [] => null
                            | _ => Message.FatalError($"Multiple '$paramName' in model params");
                        }
                    }
                    def matchAttrs(attrs : list[PExpr]) : string
                    {
                        | <[ Model(..$args) ]> :: _ => matchArgs(args, paramName)
                        | _ :: tail => matchAttrs(tail)
                        | [] => null
                    }

                    def attrs = typeBuilder.GetModifiers().GetCustomAttributes();
                    def m = matchAttrs(attrs);
                    m ?? typeBuilder.Name
                }
                DatabaseManagerImpl.RegisterModelType(tb, getName(tb, "Table"), tb.FullName)
            });

            unless (mvcBuilder.Manager.CoreEnv.OpenNamespaces.Any(t => t.FullName == ns))
                _ = mvcBuilder.Manager.CoreEnv.AddOpenNamespace(ns, Location.Default);
            def env = mvcBuilder.Manager.CoreEnv.EnterIntoNamespace(ns);
            
            def modelNames = tbs.Map(_.Name);
            when (NRailsInitsImpl.GenerateMissingModelsFromDbSchema)
            {
                def newTbs = tableNames.Except(modelNames).Map(t => {
                    def tb = env.Define(<[ decl:
                        public class $(t : usesite) { }
                    ]>);
                    DatabaseManagerImpl.RegisterModelType(tb, tb.Name, tb.FullName);
                    tb
                });
                tbs = tbs.Append(newTbs);
            }

            tbs.Iter(tb => {
                ModelImpl.GenerateColumns(tb);
                tb.Compile();
            });

            DatabaseManagerImpl.GenerateTables(dbManagerBuilder);
            dbManagerBuilder.Compile();
        }
        catch 
        {
            | e => Message.Error($"ProcessModels error: $(e.ToString())");
        }
    }
    
    fixFieldName(fileName : string) : string
    {
        def name = fileName.Replace('.', '_').Replace('-', '_');
        match (char.IsLetter(name[0]))
        {
            | true => name
            | false => $"_$name"
        }
    }

    ProcessFiles(getFilesInfo : string -> list[string*string], fileNameFixer : string -> string, dirName : string, tb : TypeBuilder) : void
    {
        def projectPath = Path.GetDirectoryName(tb.Manager.Options.ProjectPath);
        def path = Path.Combine(projectPath, dirName);

        when (Directory.Exists(path))
        {
            def separators = array [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
    
            def name = fixFieldName(dirName.Split(separators).Last());

            def helperType = tb.DefineNestedType(<[decl: 
                public module $(name : dyn) 
                {
                }
            ]>);

            def makeDecl(field)
            {
                def (field, value) = field;
                def field = fixFieldName(Path.GetFileName(field));

                assert(value.Length > projectPath.Length + 1);

                def relName = value.Substring(projectPath.Length + 1);
                def value = $"~/$relName".Replace('\\', '/'); // !!
                <[decl:
                    public static $(field : dyn) : string = $(value : string);
                ]>
            }

            getFilesInfo(path).Iter(f => {
                def (fileName, value) = f;
                def fixed = (fileNameFixer(fileName), value);
                def decl = makeDecl(fixed);
                helperType.Define(decl);
            });

            Directory.GetDirectories(path).Iter(d => ProcessFiles(getFilesInfo, fileNameFixer, Path.Combine(dirName, Path.GetFileName(d)), helperType));

            hideEquals(helperType);
            helperType.Compile();
        }
    }

    ProcessContent(tb : TypeBuilder) : void
    {
        def fileNameFixer(fileName)
        {
            fileName
        }
        def getFilesInfo(path)
        {
            Directory.GetFiles(path).Map(t => (t, t)).ToNList()
        }
        ProcessFiles(getFilesInfo, fileNameFixer, "Content", tb)
    }

    ProcessScripts(tb : TypeBuilder) : void
    {
        def fixFileName(fileName)
        {
            regexp match (fileName)
            {
                | @"^(?<name>.+)\.js$" => name
                | _ => fileName
            }
        }
        def getFilesInfo(path)
        {
            def files = NList.ToList(Directory.GetFiles(path).OrderByDescending(f => f.Length));

            def isDebug = tb.Manager.Options.IsConstantDefined("DEBUG");
            def matchFiles(files)
            {
                | file :: tail =>
                    regexp match (file)
                    {
                        | @"^(?<name>.+)(\-vsdoc|\.debug)\.js$" =>
                            def shortname = $"$name.js";
                            match (tail.Contains(shortname))
                            {
                                | true => (shortname, if (isDebug) file else shortname) :: matchFiles(tail.Remove(shortname))
                                | false => (file, file) :: matchFiles(tail)
                            }
                        | _ => (file, file) :: matchFiles(tail)
                    }
                | [] => []
            }
            matchFiles(files)
        }
        ProcessFiles(getFilesInfo, fixFileName, "Scripts", tb)
    }

    ProcessController(controllerBilder : TypeBuilder, mvcBuilder : TypeBuilder) : void
    {
        def name = controllerPredicate.Replace(controllerBilder.Name, String.Empty);
        
        def makeActionMethods(actionMethod)
        {
            def methodName = actionMethod.Name;
            def parms = actionMethod.GetParameters().Map(m => m.AsParsed());
            
            def simpleMethod = <[decl:
                public $(methodName : dyn)() : INRActionResult
                {
                    NRActionResult(String.Empty, $(name : string), $(methodName : string))
                }
            ]>;
            
            def parametrizedMethod(parms)
            {
                def parametrizedBody = 
                    <[def r = NRActionResult(String.Empty, $(name : string), $(methodName : string));]> 
                    :: parms.Map(m => <[r.RouteValueDictionary.Add($(m.Name : string), $(m.Name : dyn));]>);
                def body = parametrizedBody + [<[r]>];
                <[decl:
                    public $(methodName : dyn)(..$(parms)) : INRActionResult
                    {
                        ..$body;
                    }
                ]>
            }
            
            match (parms)
            {
                | [] => [simpleMethod]
                | _  => [simpleMethod, parametrizedMethod(parms)]
            }
        }
        
        
        def actionFilter(m)
        {
            m != null && m.ReturnType != null &&
                         typeof(ActionResult).Equals(m.ReturnType.GetSystemType())
        }
                 
        def methods = controllerBilder.GetMethods()
            .Filter(actionFilter)
            .Map(makeActionMethods)
            .Flatten();
            
        def removeDups(_)
        {
          | x :: tail =>
            match (tail.Find(m => m.Name == x.Name && m.header.Parameters.Length == x.header.Parameters.Length))
            {
              | Some => removeDups(tail)
              | None => x :: removeDups(tail)
            }
          | [] => [] 
        }
        
        def methods = removeDups(methods);

        def injectMethods(t)
        {
            foreach (method in methods)
                t.Define(method);
            
            hideEquals(t);
        }
        
        def helperType = mvcBuilder.DefineNestedType(<[decl: 
            public module $(name : dyn) 
            {
            }
        ]>);
        
        def actionType = controllerBilder.DefineNestedType(<[decl:
            public module Actions
            {
            }
        ]>);
        
        controllerBilder.Define(<[decl:
            private RedirectToAction(action :INRActionResult) : ActionResult
            {
                RedirectResult(Url.Action(action.Action, action.Controller, action.RouteValueDictionary));
            }
        ]>);
        
        [helperType, actionType].Iter(t => {
            injectMethods(t);
            t.Compile()
        })
    }

    // hiding unused methods from intellesense list
    hideEquals(t : TypeBuilder) : void
    {
        t.Define(<[decl: new private Equals(_ : object, _ : object) : bool { true } ]>);
        t.Define(<[decl: new private ReferenceEquals(_ : object, _ : object) : bool { true } ]>);
    }
  }
}
